module VirtualCity.Scripts.Planner

open Casanova
open Casanova.Utilities
open Casanova.Core
open Casanova.Coroutines
open Casanova.Game
open Casanova.Input
open Casanova.Math
open Casanova.Drawing
open VirtualCity.City
open VirtualCity.CityDescription
open VirtualCity.Plan
open VirtualCity.PlanDescription
open Wintellect.PowerCollections
open VirtualCity.Scripts.Layer1
open VirtualCity.Scripts.Layer2


let inline (!) x = immediate_lookup x


type ActionClass<'a> =
  {
    GetActions         : CityExpectations -> AgentDescription -> list<'a>
    CheckPreconditions : CityExpectations -> AgentDescription -> 'a -> bool
    IsAcceptable       : PlanDescription<'a> -> bool
    ReachesGoal        : PlanDescription<'a> -> bool
    ComparePlans       : PlanDescription<'a> -> PlanDescription<'a> -> int
    SimulateAction     : CityExpectations -> AgentDescription -> 'a -> StepExpectations
    ToString           : 'a -> string
  }


let find_plan<'a> (action_class:ActionClass<'a>) (city:CityExpectations) (agent:AgentDescription) =  
  let plan_comparison = System.Comparison<PlanDescription<'a>>(action_class.ComparePlans)
  let Q = OrderedBag(plan_comparison)
  let add_to_q plan = 
    if plan |> action_class.IsAcceptable then
      Q.Add plan
  let add_next_action (city:CityDescription) (agent:AgentDescription) (plan:PlanDescription<'a>) =
    let enqueue (action:'a) =
      if action_class.CheckPreconditions city agent action then
        let expectations = action_class.SimulateAction city agent action
        let new_steps = { Action = action; Expectations = expectations } :: plan.Steps
        add_to_q { plan with Steps =  new_steps }
    let next_actions = action_class.GetActions city agent
    for action in next_actions do
      do enqueue action
  do add_next_action city agent { Steps = [] }
//  let log_file = new System.IO.StreamWriter("log.txt")
  let rec explore_plan cnt = 
    co{
      if Q.Count = 0 then 
  //      do log_file.Close()
        return None
      else
        let plan = Q.RemoveLast()
  //      for a in plan.Steps |> List.rev do
  //        log_file.Write((a.Action |> action_class.ToString) + ";")
  //      log_file.WriteLine("\n")
        if plan |> action_class.ReachesGoal then
  //        do log_file.Close()
          return Some { plan with Steps = plan.Steps |> List.rev }
        else
          let city = plan.Steps.Head.Expectations.City
          let agent = plan.Steps.Head.Expectations.Agent
          do add_next_action city agent plan
          if cnt % 100 = 0 then
            do! yield_
          return! explore_plan (cnt+1)
      }
  explore_plan 0


let get_city_description (city:City) = 
  {
    Buses     = []
//                [ for bus in city.Buses do 
//                    let stops = 
//                      [ for s in bus.Stops do yield s.Base ]
//                    yield !bus.Physics.Position, bus.Base, stops ]
    Buildings = [ for building in city.Buildings do
                    let items = 
                      [ for item in !building.Items do yield item.Base ]
                    yield items, building.Base ]
    Roads     = [ for road in city.Roads do yield road.Base ]
    Clock     = { Time = !city.Clock.Time }
    WalkGraph = city.WalkGraph
  }


let get_agent_description (agent:Agent) = 
  {
    Position  = !agent.Physical.Position
    Stats     = !agent.Stats
    Money     = !agent.Money
    MaxSpeed  = 5.0f<km/h> |> to_meters_per_second
    Inventory = [ for item in agent.Inventory do yield item.Base ]
    Work      = (!agent.Work).Base
    Home      = (!agent.Home).Base
  }


let rec create_plan city (agent:Agent) =
  co{
    let stats_ok = (!agent.Stats).Rested > 0.0f && (!agent.Stats).Sated > 0.0f
    let plan_delay = 1800.0f<s>
    let agent_description = agent |> get_agent_description
    let city_description = city |> get_city_description
    let city_description,agent_description = update_city plan_delay city_description Stats<1>.One agent_description

    let layer2_action = 
      {
        GetActions         = useable_items
        CheckPreconditions = layer2_preconditions_ok
        IsAcceptable       = layer2_is_acceptable
        ReachesGoal        = layer2_reaches_goal
        SimulateAction     = layer2_simulate_action
        ComparePlans       = layer2_compare_plans
        ToString           = (fun a -> a.ToString())
      }

    do agent.CurrentAction := "Planning"
    let closest_building = city.Buildings |> Seq.minBy (fun b -> Vector2.Distance(b.Position, !agent.Physical.Position))
    do agent.Physical.Target := closest_building.Position
    do! co{
          let! plan = find_plan layer2_action city_description agent_description
          match plan with
          | Some plan -> 
            do agent.CurrentAction := "Planning travels"
//            if !agent.Selected then
//              printfn "The higher-level plan is: %s.\n\nPlanning travels" (plan.ToString())
            let locations =
              [
                yield city_description,agent_description
                for step in plan.Steps do
                  yield step.Expectations.City,step.Expectations.Agent
              ]
            let plan_failed = var false
            let final_travel_plans = var []
            for (city_description,agent_description),next_step in Seq.zip locations plan.Steps do
              if !plan_failed |> not then
                let source = agent_description.Position
                let target = next_step.Expectations.Agent.Position
                let get_actions city agent =
                  [
                    //for bus in buses_nearby city agent do yield Layer1ActionDescription.Board bus
                    for target in targets_nearby target city agent do yield Layer1ActionDescription.Walk target
                  ]
                let layer1_action = 
                  {
                    GetActions         = get_actions
                    CheckPreconditions = layer1_preconditions_ok
                    IsAcceptable       = layer1_is_acceptable next_step.Expectations.City.Clock.Time
                    ReachesGoal        = layer1_reaches_goal source target
                    SimulateAction     = layer1_simulate_action
                    ComparePlans       = layer1_compare_plans source target
                    ToString           = (fun a -> a.ToString())
                  }
                if Vector2.Distance(source,target) <= 2.0f<m> then
                  final_travel_plans := ([], next_step) :: !final_travel_plans
                else
                  let! travel_plans = find_plan layer1_action city_description agent_description
                  match travel_plans with
                  | Some travel_plans -> 
                    let reached_location = travel_plans.Steps.Last.Expectations.Agent.Position
                    final_travel_plans := (travel_plans.Steps, next_step) :: !final_travel_plans
                  | None -> 
//                    if !agent.Selected then
//                      printfn "Failed planning travels"
                    plan_failed := true // we proceed anyway, replanning will be triggered at the first unreachable use-item action

            let travel_plans = !final_travel_plans |> List.rev

            let of_layer_2 step =
              { 
                Action = 
                  match step.Action with
                  | UseAndTravel(location,item,use_effect_index) -> Use(location,item,use_effect_index)
                Expectations = step.Expectations
              }
            let of_layer_1 step =
              { 
                Action = 
                  match step.Action with
                  | Layer1ActionDescription.Walk target -> Walk target
                  //| Layer1ActionDescription.Board(bus,source,dest)   -> Board(bus,source,dest)
                Expectations = step.Expectations
              }

            let new_steps = 
              [
                for travel_plan,step in travel_plans do
                  yield! travel_plan |> Seq.map of_layer_1
                  yield step |> of_layer_2
              ]
        
            let plan_is_deadly = new_steps |> Seq.exists (fun s -> s.Expectations.Agent.Stats.VitalMin <= 0.0f)
            if plan_is_deadly then
              do printfn "Plan\n %s\n would cause agent %s's death. Replanning." (({ Steps = new_steps }).ToString()) agent.Name

            if (!agent.Stats).VitalMin > 0.0f && plan_is_deadly then
              agent.ActivePlan := 
                {
                  Steps = []
                }
//              if !agent.Selected then
              do printfn "Plan would cause agent %s's death. Replanning." agent.Name
              do! wait 1800.0f<s>
            else
              agent.ActivePlan := 
                {
                  Steps = new_steps
                }

//              if !agent.Selected then
//                do printfn "Travels planned.\nThe selected plan is:\n%s" ((!agent.ActivePlan).ToString())

              do agent.CurrentAction := "Starting..."
              do! wait_condition (fun () -> !city.Clock.Time >= city_description.Clock.Time)

          | _ -> 
            if Vector2.Distance(!agent.Physical.Position, (!agent.Home).Position) > 1.0f<m> then
              agent.Physical.Target := (!agent.Home).Position
            agent.ActivePlan := 
              {
                Steps = []
              }

            do agent.CurrentAction := "Replanning"
        } .||> wait plan_delay
  }
